<?php

class Debug
{
	/**
	 * @param   mixed   $var,...    variable to debug
	 * @return  string
	 */
	public static function vars()
	{
		if (func_num_args() === 0)
			return;

		// Get all passed variables
		$variables = func_get_args();

		$output = array();
		foreach ($variables as $var)
		{
			$output[] = Debug::_dump($var, 1024);
		}

		return '<pre class="debug">' . implode("\n", $output) . '</pre>';
	}

	/**
	 * @param   mixed   $value              variable to dump
	 * @param   integer $length             maximum length of strings
	 * @param   integer $level_recursion    recursion limit
	 * @return  string
	 */
	public static function dump($value, $length = 128, $level_recursion = 10)
	{
		return Debug::_dump($value, $length, $level_recursion);
	}

	/**
	 * Helper for Debug::dump(), handles recursion in arrays and objects.
	 *
	 * @param   mixed   $var    variable to dump
	 * @param   integer $length maximum length of strings
	 * @param   integer $limit  recursion limit
	 * @param   integer $level  current recursion level (internal usage only!)
	 * @return  string
	 */
	protected static function _dump(&$var, $length = 128, $limit = 10, $level = 0)
	{
		if ($var === NULL)
		{
			return '<small>NULL</small>';
		}
		elseif (is_bool($var))
		{
			return '<small>bool</small> ' . ($var ? 'TRUE' : 'FALSE');
		}
		elseif (is_float($var))
		{
			return '<small>float</small> ' . $var;
		}
		elseif (is_resource($var))
		{
			if (($type = get_resource_type($var)) === 'stream' AND $meta = stream_get_meta_data($var))
			{
				$meta = stream_get_meta_data($var);

				if (isset($meta['uri']))
				{
					$file = $meta['uri'];

					if (function_exists('stream_is_local'))
					{
						// Only exists on PHP >= 5.2.4
						if (stream_is_local($file))
						{
							$file = Debug::getPath($file);
						}
					}

					return '<small>resource</small><span>(' . $type . ')</span> ' . htmlspecialchars($file, ENT_NOQUOTES, APP_CHARSET);
				}
			}
			else
			{
				return '<small>resource</small><span>(' . $type . ')</span>';
			}
		}
		elseif (is_string($var))
		{
			// Clean invalid multibyte characters. iconv is only invoked
			// if there are non ASCII characters in the string, so this
			// isn't too much of a hit.
			$var = UTF8::clean($var, APP_CHARSET);

			if (UTF8::strlen($var) > $length)
			{
				// Encode the truncated string
				$str = htmlspecialchars(UTF8::substr($var, 0, $length), ENT_NOQUOTES, APP_CHARSET) . '&nbsp;&hellip;';
			}
			else
			{
				// Encode the string
				$str = htmlspecialchars($var, ENT_NOQUOTES, APP_CHARSET);
			}

			return '<small>string</small><span>(' . strlen($var) . ')</span> "' . $str . '"';
		}
		elseif (is_array($var))
		{
			$output = array();

			// Indentation for this variable
			$space = str_repeat($s = '    ', $level);

			static $marker;

			if ($marker === NULL)
			{
				// Make a unique marker
				$marker = uniqid("\x00");
			}

			if (empty($var))
			{
				// Do nothing
			}
			elseif (isset($var[$marker]))
			{
				$output[] = "(\n$space$s*RECURSION*\n$space)";
			}
			elseif ($level < $limit)
			{
				$output[] = "<span>(";

				$var[$marker] = TRUE;
				foreach ($var as $key => &$val)
				{
					if ($key === $marker)
						continue;
					if (!is_int($key))
					{
						$key = '"' . htmlspecialchars($key, ENT_NOQUOTES, APP_CHARSET) . '"';
					}

					$output[] = "$space$s$key => " . Debug::_dump($val, $length, $limit, $level + 1);
				}
				unset($var[$marker]);

				$output[] = "$space)</span>";
			}
			else
			{
				// Depth too great
				$output[] = "(\n$space$s...\n$space)";
			}

			return '<small>array</small><span>(' . count($var) . ')</span> ' . implode("\n", $output);
		}
		elseif (is_object($var))
		{
			// Copy the object as an array
			$array = (array)$var;

			$output = array();

			// Indentation for this variable
			$space = str_repeat($s = '    ', $level);

			$hash = spl_object_hash($var);

			// Objects that are being dumped
			static $objects = array();

			if (empty($var))
			{
				// Do nothing
			}
			elseif (isset($objects[$hash]))
			{
				$output[] = "{\n$space$s*RECURSION*\n$space}";
			}
			elseif ($level < $limit)
			{
				$output[] = "<code>{";

				$objects[$hash] = TRUE;
				foreach ($array as $key => &$val)
				{
					if ($key[0] === "\x00")
					{
						// Determine if the access is protected or protected
						$access = '<small>' . (($key[1] === '*') ? 'protected' : 'private') . '</small>';

						// Remove the access level from the variable name
						$key = substr($key, strrpos($key, "\x00") + 1);
					}
					else
					{
						$access = '<small>public</small>';
					}

					$output[] = "$space$s$access $key => " . Debug::_dump($val, $length, $limit, $level + 1);
				}
				unset($objects[$hash]);

				$output[] = "$space}</code>";
			}
			else
			{
				// Depth too great
				$output[] = "{\n$space$s...\n$space}";
			}

			return '<small>object</small> <span>' . get_class($var) . '(' . count($array) . ')</span> ' . implode("\n", $output);
		}
		else
		{
			return '<small>' . gettype($var) . '</small> ' . htmlspecialchars(print_r($var, TRUE), ENT_NOQUOTES, APP_CHARSET);
		}
	}

	/**
	 * @param   string  $file   path to debug
	 * @return  string
	 */
	public static function getPath($file)
	{
		return str_replace(array(APP_ROOT_PATH,SYSTEM_ROOT_PATH), array('APP/','SYSTEM/'), $file);

		return $file;
	}

	/**
	 * @param   string  $file           file to open
	 * @param   integer $line_number    line number to highlight
	 * @param   integer $padding        number of padding lines
	 * @return  string   source of file
	 * @return  FALSE    file is unreadable
	 */
	public static function source($file, $line_number, $padding = 5)
	{
		if (!$file OR !is_readable($file))
		{
			// Continuing will cause errors
			return FALSE;
		}

		// Open the file and set the line position
		$file = fopen($file, 'r');
		$line = 0;

		// Set the reading range
		$range = array(
			'start' => $line_number - $padding,
			'end' => $line_number + $padding
		);

		// Set the zero-padding amount for line numbers
		$format = '% ' . strlen($range['end']) . 'd';

		$source = '';
		while (($row = fgets($file)) !== FALSE)
		{
			// Increment the line number
			if (++$line > $range['end'])
				break;

			if ($line >= $range['start'])
			{
				// Make the row safe for output
				$row = htmlspecialchars($row, ENT_NOQUOTES, APP_CHARSET);

				// Trim whitespace and sanitize the row
				$row = '<span class="number">' . sprintf($format, $line) . '</span> ' . $row;

				if ($line === $line_number)
				{
					// Apply highlighting to this row
					$row = '<span class="line highlight">' . $row . '</span>';
				}
				else
				{
					$row = '<span class="line">' . $row . '</span>';
				}

				// Add to the captured source
				$source .= $row;
			}
		}

		// Close the file
		fclose($file);

		return '<pre class="source"><code>' . $source . '</code></pre>';
	}

	/**
	 * 
	 * @param   array   $trace
	 * @return  string
	 */
	public static function getTraceHtml(array $trace = NULL)
	{
		if ($trace === NULL)
		{
			// Start a new trace
			$trace = debug_backtrace();
		}

		// Non-standard function calls
		$statements = array(
			'include',
			'include_once',
			'require',
			'require_once'
		);

		$output = array();
		foreach ($trace as $step)
		{
			if (!isset($step['function']))
			{
				// Invalid trace step
				continue;
			}

			if (isset($step['file']) AND isset($step['line']))
			{
				// Include the source of this step
				$source = Debug::source($step['file'], $step['line']);
			}

			if (isset($step['file']))
			{
				$file = $step['file'];

				if (isset($step['line']))
				{
					$line = $step['line'];
				}
			}

			// function()
			$function = $step['function'];

			if (in_array($step['function'], $statements))
			{
				if (empty($step['args']))
				{
					// No arguments
					$args = array();
				}
				else
				{
					// Sanitize the file path
					$args = array($step['args'][0]);
				}
			}
			elseif (isset($step['args']))
			{
				if (!function_exists($step['function']) OR strpos($step['function'], '{closure}') !== FALSE)
				{
					// Introspection on closures or language constructs in a stack trace is impossible
					$params = NULL;
				}
				else
				{
					if (isset($step['class']))
					{
						if (method_exists($step['class'], $step['function']))
						{
							$reflection = new ReflectionMethod($step['class'], $step['function']);
						}
						else
						{
							$reflection = new ReflectionMethod($step['class'], '__call');
						}
					}
					else
					{
						$reflection = new ReflectionFunction($step['function']);
					}

					// Get the function parameters
					$params = $reflection -> getParameters();
				}

				$args = array();

				foreach ($step['args'] as $i => $arg)
				{
					if (isset($params[$i]))
					{
						// Assign the argument by the parameter name
						$args[$params[$i] -> name] = $arg;
					}
					else
					{
						// Assign the argument by number
						$args[$i] = $arg;
					}
				}
			}

			if (isset($step['class']))
			{
				// Class->method() or Class::method()
				$function = $step['class'] . $step['type'] . $step['function'];
			}

			$output[] = array(
				'function' => $function,
				'args' => isset($args) ? $args : NULL,
				'file' => isset($file) ? $file : NULL,
				'line' => isset($line) ? $line : NULL,
				'source' => isset($source) ? $source : NULL,
			);

			unset($function, $args, $file, $line, $source);
		}

		return var_export($output, 1);
	}
	
	/**
	 * get debug trace back
	 * @return string
	 */
	static public function getTrace()
	{
		$backtrace = array_slice(debug_backtrace(), 1);
		$output = '';

		foreach ($backtrace as $index => $stack)
		{
			// Process args
			$args = array();
			if (!empty($stack['args']))
			{
				foreach ($stack['args'] as $argIndex => $argValue)
				{
					if (is_object($argValue))
					{
						$args[$argIndex] = get_class($argValue);
					}
					else
					if (is_array($argValue))
					{
						$args[$argIndex] = 'Array';
						//substr(print_r($argValue, true), 0, 32);
					}
					else
					if (is_string($argValue))
					{
						$args[$argIndex] = "'" . substr($argValue, 0, 32) . (strlen($argValue) > 32 ? '...' : '') . "'";
					}
					else
					{
						$args[$argIndex] = print_r($argValue, true);
					}
				}
			}
			// Process message
			$output .= sprintf('#%1$d %2$s(%3$d): %4$s%5$s%6$s(%7$s)', $index, (!empty($stack['file']) ? Debug::getPath($stack['file']) : '(unknown file)'), (!empty($stack['line']) ? $stack['line'] : '(unknown line)'), (!empty($stack['class']) ? $stack['class'] : ''), (!empty($stack['type']) ? $stack['type'] : ''), $stack['function'], join(', ', $args)) . PHP_EOL;
		}

		// Throw main in there for the hell of it
		$output .= sprintf('#%1$d {main}', $index + 1);

		return $output;
	}
}
